package com.swak.utils.bean;

import java.beans.PropertyDescriptor;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.util.Assert;

import com.swak.asm.ClassGenerator;
import com.swak.utils.Methods;
import com.swak.utils.ReflectUtils;

import net.sf.cglib.core.MethodInfo;

/**
 * 缺点：运行期间生成转换器类，使用懒加载；
 * 這是一款很簡單的轉換器；通过 get、set来提升来给你个对象之前的值复制（浅拷贝）；
 * 维护所有的属性转换中间类
 * 
 * @author 618lf
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class BeanCopiers {
	private static final AtomicLong PROXY_CLASS_COUNTER = new AtomicLong(0);
	private static final Map<ClassLoader, Map<String, Object>> PROXY_CACHE_MAP = new WeakHashMap<>();
	private static final Object PENDING_GENERATION_MARKER = new Object();

	/**
	 * 使用 Lazy 的方式创建处理器
	 * 
	 * @param <DTO>
	 * @param <PO>
	 * @param dtoClazz
	 * @param poClazz
	 * @return
	 */
	public static <DTO, PO> BeanCopier<DTO, PO> create(Class<DTO> dtoClazz, Class<PO> poClazz) {
		Assert.isTrue(dtoClazz != null, "DTO type is null!");
		Assert.isTrue(poClazz != null, "PO type is null!");
		return new LazyBeanCopier(dtoClazz, poClazz);
	}
	
	static <DTO, PO> BeanCopier<DTO, PO> createCopier(Class<DTO> dtoClazz, Class<PO> poCLazz) {

		// Key
		String key = new StringBuilder().append(dtoClazz.getPackage().getName()).append(".")
				.append(dtoClazz.getSimpleName()).append("_").append(poCLazz.getSimpleName()).toString();

		// get cache by class loader.
		final Map<String, Object> cache;
		synchronized (PROXY_CACHE_MAP) {
			cache = PROXY_CACHE_MAP.computeIfAbsent(dtoClazz.getClassLoader(), k -> new HashMap<>());
		}

		BeanCopier proxy = null;
		synchronized (cache) {
			do {
				Object value = cache.get(key);
				if (value instanceof Reference<?>) {
					proxy = (BeanCopier) ((Reference<?>) value).get();
					if (proxy != null) {
						return proxy;
					}
				}

				if (value == PENDING_GENERATION_MARKER) {
					try {
						cache.wait();
					} catch (InterruptedException e) {
					}
				} else {
					cache.put(key, PENDING_GENERATION_MARKER);
					break;
				}
			} while (true);
		}

		long id = PROXY_CLASS_COUNTER.getAndIncrement();
		ClassGenerator ccp = null;
		try {

			String ccp_name = key + "$$BeanCopier" + id;
			ccp = ClassGenerator.newInstance(dtoClazz.getClassLoader());
			ccp.setClassName(ccp_name);
			ccp.addInterface(BeanCopier.class);

			Method[] methods = BeanCopier.class.getDeclaredMethods();
			int size = methods.length;
			if (methods != null && methods.length > 0) {
				for (int i = 0; i < size; i++) {
					Method method = methods[i];
					if (!Methods.canProxy(method)) {
						continue;
					}
					Class<?> rt = method.getReturnType();
					Class<?>[] pts = method.getParameterTypes();
					ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(),
							createCode(method, dtoClazz, poCLazz));
				}
			}

			Class<?> clazz = ccp.toClass();
			proxy = (BeanCopier) clazz.getDeclaredConstructor().newInstance();
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		} finally {
			// release ClassGenerator
			if (ccp != null) {
				ccp.release();
			}
			synchronized (cache) {
				if (proxy == null) {
					cache.remove(key);
				} else {
					cache.put(key, new WeakReference<Object>(proxy));
				}
				cache.notifyAll();
			}
		}
		return proxy;
	}

	private static <DTO, PO> String createCode(Method method, Class<DTO> dtoClazz, Class<PO> poCLazz) {
		if (method.getName().equals("transform")) {
			return createTransformCode(method, dtoClazz, poCLazz);
		}
		return createConvertCode(method, dtoClazz, poCLazz);
	}

	private static <DTO, PO> String createTransformCode(Method method, Class<DTO> dtoClazz, Class<PO> poCLazz) {
		PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(dtoClazz);
		PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(poCLazz);
		Map names = new HashMap();
		for (int i = 0; i < getters.length; i++) {
			names.put(getters[i].getName(), getters[i]);
		}
		StringBuilder code = new StringBuilder();
		code.append(poCLazz.getName()).append(" po = new ").append(poCLazz.getName()).append("();");
		code.append(dtoClazz.getName()).append(" dto = (").append(dtoClazz.getName()).append(")$1;");
		for (int i = 0; i < setters.length; i++) {
			PropertyDescriptor setter = setters[i];
			PropertyDescriptor getter = (PropertyDescriptor) names.get(setter.getName());
			if (getter != null && compatible(getter, setter)) {
				MethodInfo read = net.sf.cglib.core.ReflectUtils.getMethodInfo(getter.getReadMethod());
				MethodInfo write = net.sf.cglib.core.ReflectUtils.getMethodInfo(setter.getWriteMethod());
				code.append("po.").append(write.getSignature().getName()).append("((")
						.append(getter.getPropertyType().getName()).append(")dto.")
						.append(read.getSignature().getName()).append("());");
			}
		}
		code.append("return po;");
		return code.toString();
	}

	private static <DTO, PO> String createConvertCode(Method method, Class<DTO> dtoClazz, Class<PO> poCLazz) {
		PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(poCLazz);
		PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(dtoClazz);
		Map names = new HashMap();
		for (int i = 0; i < getters.length; i++) {
			names.put(getters[i].getName(), getters[i]);
		}
		StringBuilder code = new StringBuilder();
		code.append(dtoClazz.getName()).append(" dto = new ").append(dtoClazz.getName()).append("();");
		code.append(poCLazz.getName()).append(" po = (").append(poCLazz.getName()).append(")$1;");
		for (int i = 0; i < setters.length; i++) {
			PropertyDescriptor setter = setters[i];
			PropertyDescriptor getter = (PropertyDescriptor) names.get(setter.getName());
			if (getter != null && compatible(getter, setter)) {
				MethodInfo read = net.sf.cglib.core.ReflectUtils.getMethodInfo(getter.getReadMethod());
				MethodInfo write = net.sf.cglib.core.ReflectUtils.getMethodInfo(setter.getWriteMethod());
				code.append("dto.").append(write.getSignature().getName()).append("((")
						.append(getter.getPropertyType().getName()).append(")po.").append(read.getSignature().getName())
						.append("());");
			}
		}
		code.append("return dto;");
		return code.toString();
	}

	private static boolean compatible(PropertyDescriptor getter, PropertyDescriptor setter) {
		return setter.getPropertyType().isAssignableFrom(getter.getPropertyType());
	}
}